// NOTICE - The only repository for up-to-date RMM code is now stored in Google code at http://code.google.com/p/rich-mail-merge/
// ATTENTION - Le seul emplacement pour le code actualisé de RMM est maintenant Google code http://code.google.com/p/rich-mail-merge/


// Created by Patrick Oliviero
// www.revolution-informatique.fr
// based on 'Simple mail Merge' Google apps script tutorial.
// Use a google site page as the Email template !
// sendEmails() function updated
// readPage(domain,site,page) added

// updated 02/14/2011 v2.0 
// release notes http://code.google.com/p/rich-mail-merge/wiki/RmmReleases
 
// Créé par Patrick Oliviero
// www.revolution-informatique.fr
// basé sur la formation Google apps script 'Simple Mail Merge'
// utilise une page Google site comme modèle de l'email !
// fonction sendEmails() mise à jour
// fonction readPage(domaine,site,page)ajoutée

// Mis à jour 14/02/2011
// notes de version hhttp://code.google.com/p/rich-mail-merge/wiki/RmmReleases

function sendEmails() {
 var ss = SpreadsheetApp.getActiveSpreadsheet();
 var dataSheet = ss.getSheets()[0];
 // for backward compatibility reasons the # version must be on the 1st row
 // pour asssurer la compatibilité descendante le n° de version doit être sur la 1ere ligne
 var rmmVersion = getRmmVersion(dataSheet); 
  if (rmmVersion=="v1.0") {
    //deprecated version 1.0 headers are on the first row
    // version 1.0 dépréciée les entêtes sont sur la première ligne
    var domainName = 'revolution-informatique.fr'; // uddate with yourDomain.com - mettre à jour avec votreDommaine.fr
    var siteName='revolution-informatique-script'; // update with yourSite.com - mettre à jour avec votreSite.fr
    var pageName = '/newsletters/n201102';      // update with yourPage name - mettre à jour avec votre page
    var emailSubject = "Lettre information Révolution Informatique"; //update with your Subject - mettre à jour avec votre sujet
    var language = "fr"; //update with your language code - mettre à jour avec votre code langage
    // contacts list starts at the 2nd row
    // la liste des contacts commence sur la 2eme ligne
    var dataRange = dataSheet.getRange(2, 1,dataSheet.getLastRow(),dataSheet.getLastColumn());
   } else {
    // version 2.0 - 2 rows for headers
    // on the 1st row are headers inherited from the form, versionning and parameters headers
    // on the 2nd are fiels names (whitch are now independant from colums names) & parameters values

    // version 2.0 - 2 lignes d'entête
    // La 1ere ligne contient les noms de colonnes hérités du formulaire, le n° de version  et les noms de paramètres
     // la 2eme ligne contient les nom de champs (maintenant indépendants des noms de colonnes) et les valeurs des paramètres
     
    // Read parameters from dataSheet
    // lecture des paramètres à partir de la feuille de calcul
    var dataRange = dataSheet.getRange(2, 1, 2,dataSheet.getLastColumn());
    // Create one JavaScript object from the row of data
    // Crée un objet Javascript à partir des données de la ligne
    objects = getRowsData(dataSheet, dataRange); 
    var data = objects[0];
    var domainName = data["domainName"];
    var siteName=data["siteName"];
    var pageName = data["pageName"];
    var emailSubject = data["emailSubject"];
    var language = data["language"];
    // contacts list starts at the 3rd row
    // la liste des contacts commence à la 3eme ligne
    var dataRange = dataSheet.getRange(3, 1,dataSheet.getLastRow(),dataSheet.getLastColumn());
 }
 

 // Read the Template
 // Lit le modèle
 var emailTemplate = readPage(domainName,siteName,pageName);
 //update the variables
 //met à jour les variables
 emailTemplate=fillInTemplateVariables(emailTemplate,language);
 
  // Create one JavaScript object per row of data.
 // crée un objet javaScript pour chaque ligne de données
 objects = getRowsData(dataSheet, dataRange);
 
 // For every row object, create a personalized email from a template and send
 // it to the appropriate person.
 // pour chaque ligne d'objet, crée un email personnalisé à partir du modèle et
 // l'envoie au contact
 for (var i = 0; i < objects.length; ++i) {
   // Get a row object
   // récupération de l'objet
   var rowData = objects[i];
 
   // Generate a personalized email.
   // Given an HTML template, replace markers (for instance ${"Name"}) with
   // the corresponding value in a row object (for instance rowData.Name).
   // Génère un email personnalisé.
   // à partir du modèle HTML, remplace les champs (ex ${"nom"} par la 
   // valeur correspondante d'un objet (ex rowData.name}
   var emailText = fillInTemplateFromObject(emailTemplate, rowData);
 
   MailApp.sendEmail(rowData.email, emailSubject, "", {htmlBody:emailText});
   }
// You can uncomment the next line to get a confirmation 
  //Browser.msgBox(i+"Email(s) sent : ", Browser.Buttons.OK);
// Vous pouvez rendre la ligne suivante active pour avoir une confirmation de l'envoi
//Browser.msgBox(i+" Email(s) envoyé(s)", Browser.Buttons.OK);
}

 
function readPage(domainName,siteName,pageName) {
 // based on (basé sur) google apps script / site services documentation http://code.google.com/intl/fr-FR/googleapps/appsscript/service_sites.html
 // return the HTML page content of domainName,siteName,pageName
 // renvoie le code HTML du contenu de la page domainName,siteName,pageName (nom de domaine, nom de site, nom de page)
 var site = SitesApp.getSite(domainName,siteName);
 var page = site.getChildByName(pageName);
 var content = page.getHtmlContent();
 content=hyperlinksCorrect(content);
 return content;
}

function getRmmVersion(dataSheet) {
  // read the version number on the 1st row - 1.0 by default
  // lit le numéro de version sur la 1ere ligne - 1.0 par défaut
 var rmmVersion="v1.0";
 var dataRange = dataSheet.getRange(1,1,1,dataSheet.getLastColumn());
 headers=dataRange.getValues();
 for (var i = 0; i < dataSheet.getLastColumn(); ++i) {
   if (headers[0][i]=="version") {
     rmmVersion=headers [0][++i];
   }
 }     
 return rmmVersion;
}
     
 function hyperlinksCorrect(message) {
  // for hyperlinks replace the automatic Google Sites conversion $%7B%22MyField%22%7D by ${"MyField"}
  // pour les hyperliens remplace la conversion automatique de Google Sites $%7B%22MyField%22%7D par ${"MyField"}
  var reg=new RegExp(/\$\%7B\%22/g);
  message=message.replace(reg,"${\"");
  var reg=new RegExp(/\%22\%7D/g);
  message=message.replace(reg,"\"}");
  return message;
}

 function rangeCopy() {
 // based on (basé sur) google apps script / spreadsheet services documentation
 // http://code.google.com/intl/fr/googleapps/appsscript/service_spreadsheet.html
 // Copy a range of cells.
 // Should be used by a 'on update' trigger to add content regardless of the form
 // By example for adding formatted fields 
 // Copie une sélection de cellules. 
 // Pour utilisation par un déclencheur on update pour ajouter du contenu indépendamment du formulaire
 // Exemples d'utilisation ajout de champs formatés
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var dataSheet = ss.getSheets()[0];  
  var rangeToCopy = dataSheet.getRange("M2");
  var targetRow = dataSheet.getLastRow();
  var target = dataSheet.getRange("M"+(targetRow+1));
  rangeToCopy.copyTo(target);
  return 1;
}

// Replaces variables in a template string with corresponding values.
// Arguments:
//   - template: string containing variables, for instance %{"vName"}
//               supported variables :
//                          %{"jj"}, %{"mm"}, %{"aa"} or %{"yy"} day, month, year 2 digits
//                          %{"ddddd"} or %{"jjjjj"} day of the week
//                          %{"mmmmm"} month in letters
//                          %{"aaaa"} or %{"yyyy"} year 4 digits
//   - language: language code for day and month translation

// Remplace les variables u modèle par les valeurs correspondantes
// Paramètres :
//   - template: chaine contenant les variables, ex %{vName"}
//                variables supportées :
//                          %{"jj"}, %{"mm"}, %{"aa"} or %{"yy"} jour, mois, année 2 digits
//                          %{"ddddd"} or %{"jjjjj"} jour de la semaine
//                          %{"mmmmm"} mois en lettre
//                          %{"aaaa"} or %{"yyyy"} année 4 digits
//   - language: code langage pour la traduction des jours et mois

function fillInTemplateVariables(template,language) {
  var message = template;
  var d=new Date();
  // year 4 digits année
  var reg=new RegExp(/\%\{\"aaaa\"\}|\%\{\"yyyy\"\}/g);
  message=message.replace(reg,d.getFullYear());
  // year 2 digits année
  var reg=new RegExp(/\%\{\"aa\%\{\"|yy\"\}\"\}/g);
  message=message.replace(reg,d.getFullYear().toString().slice(-2));
  // month 2 digits mois
  var reg=new RegExp(/\%\{\"mm\"\}/g);
  message=message.replace(reg,d.getMonth());  
  // day 2 digits jour
  var reg=new RegExp(/\%\{\"jj\"\}|\%\{\"dd\"\}/g);
  message=message.replace(reg,d.getDate()); 
  // Day of the week - jour de la semaine
  var reg=new RegExp(/\%\{\"jjjjj\"\}|\%\{\"ddddd\"\}/g);
  var ddddd=Utilities.formatDate(d, "GMT", "EEEEE");
  ddddd=LanguageApp.translate(ddddd,"en",language);
  message=message.replace(reg,ddddd);
  // month in letters - mois en lettres
  var reg=new RegExp(/\%\{\"mmmmm\"\}/g);
  var mmmmm=Utilities.formatDate(d, "GMT", "MMMMM");
  mmmmm=LanguageApp.translate(mmmmm,"en",language);
  message=message.replace(reg,mmmmm);  
 return message;
}


// Replaces markers in a template string with values define in a JavaScript data object.
// Arguments:
//   - template: string containing markers, for instance ${"Column name"}
//   - data: JavaScript object with values to that will replace markers. For instance
//           data.columnName will replace marker ${"Column name"}
// Returns a string without markers. If no data is found to replace a marker, it is
// simply removed.
function fillInTemplateFromObject(template, data) {
 var email = template;
 // Search for all the variables to be replaced, for instance ${"Column name"}
 var templateVars = template.match(/\$\{\"[^\"]+\"\}/g);
 // Replace variables from the template with the actual values from the data object.
 // If no value is available, replace with the empty string.
 for (var i = 0; i < templateVars.length; ++i) {
   // normalizeHeader ignores ${"} so we can call it directly here.
   var variableData = data[normalizeHeader(templateVars[i])];
   email = email.replace(templateVars[i], variableData || "");
 }
 
 return email;
}
 
 
 
 
 
//////////////////////////////////////////////////////////////////////////////////////////
//
// The code below is reused from the 'Reading Spreadsheet data using JavaScript Objects'
// tutorial.
//
//////////////////////////////////////////////////////////////////////////////////////////
 
// getRowsData iterates row by row in the input range and returns an array of objects.
// Each object contains all the data for a given row, indexed by its normalized column name.
// Arguments:
//   - sheet: the sheet object that contains the data to be processed
//   - range: the exact range of cells where the data is stored
//   - columnHeadersRowIndex: specifies the row number where the column names are stored.
//       This argument is optional and it defaults to the row immediately above range; 
// Returns an Array of objects.
function getRowsData(sheet, range, columnHeadersRowIndex) {
 columnHeadersRowIndex = columnHeadersRowIndex || range.getRowIndex() - 1;
 var numColumns = range.getEndColumn() - range.getColumn() + 1;
 var headersRange = sheet.getRange(columnHeadersRowIndex, range.getColumn(), 1, numColumns);
 var headers = headersRange.getValues()[0];
 return getObjects(range.getValues(), normalizeHeaders(headers));
}
 
// For every row of data in data, generates an object that contains the data. Names of
// object fields are defined in keys.
// Arguments:
//   - data: JavaScript 2d array
//   - keys: Array of Strings that define the property names for the objects to create
function getObjects(data, keys) {
  var objects = [];
  for (var i = 0; i < data.length; ++i) {
   var object = {};
   var hasData = false;
   for (var j = 0; j < data[i].length; ++j) {
     var cellData = data[i][j];
     if (isCellEmpty(cellData)) {
       continue;
     }
     object[keys[j]] = cellData;
     hasData = true;
   }
   if (hasData) {
     objects.push(object);
   }
 }
 return objects;
}
 
// Returns an Array of normalized Strings.
// Arguments:
//   - headers: Array of Strings to normalize
function normalizeHeaders(headers) {
 var keys = [];
 for (var i = 0; i < headers.length; ++i) {
   var key = normalizeHeader(headers[i]);
   if (key.length > 0) {
     keys.push(key);
   }
 }
 return keys;
}
 
// Normalizes a string, by removing all alphanumeric characters and using mixed case
// to separate words. The output will always start with a lower case letter.
// This function is designed to produce JavaScript object property names.
// Arguments:
//   - header: string to normalize
// Examples:
//   "First Name" -> "firstName"
//   "Market Cap (millions) -> "marketCapMillions
//   "1 number at the beginning is ignored" -> "numberAtTheBeginningIsIgnored"
function normalizeHeader(header) {
 var key = "";
 var upperCase = false;
 for (var i = 0; i < header.length; ++i) {
   var letter = header[i];
   if (letter == " " && key.length > 0) {
     upperCase = true;
     continue;
   }
   if (!isAlnum(letter)) {
     continue;
   }
   if (key.length == 0 && isDigit(letter)) {
     continue; // first character must be a letter
   }
   if (upperCase) {
     upperCase = false;
     key += letter.toUpperCase();
   } else {
     key += letter.toLowerCase();
   }
 }
 return key;
}
 
// Returns true if the cell where cellData was read from is empty.
// Arguments:
//   - cellData: string
function isCellEmpty(cellData) {
 return typeof(cellData) == "string" && cellData == "";
}
 
// Returns true if the character char is alphabetical, false otherwise.
function isAlnum(char) {
 return char >= 'A' && char <= 'Z' ||
   char >= 'a' && char <= 'z' ||
   isDigit(char);
}
 
// Returns true if the character char is a digit, false otherwise.
function isDigit(char) {
 return char >= '0' && char <= '9';
}
